﻿using System;
using System.Diagnostics;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Text.RegularExpressions;
using System.Web;
using System.Reflection;

namespace Landscape
{
    /// <summary>
    /// The main class of Landscape, takes care of XSL rendering operations
    /// </summary>
    public class Renderer
    {
        #region Configurable properties

        /// <summary>
        /// The xslt extension (without trailing dot) used by XSLT files
        /// </summary>
        public static string xslExtension = "xslt";

        /// <summary>
        /// The XSLT namespace to use to create XSLT elements runtime
        /// </summary>
        public static string xslNS = "http://www.w3.org/1999/XSL/Transform";

        /// <summary>
        /// The XSLT version used by the framework
        /// </summary>
        public static string xslVersion = "1.0";

        /// <summary>
        /// The specific public DocType used by master templates (layouts)
        /// </summary>
        public static string xmlDocTypePublic = "-//W3C XHTML 1.0 Transitional//EN";

        /// <summary>
        /// The specific system DocType used by master templates (layouts)
        /// </summary>
        public static string xmlDocTypeSystem = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";

        /// <summary>
        /// The Landscape.OutputMethods used by master templates (layouts)
        /// </summary>
        public static OutputMethods xslOutputMethod = OutputMethods.Xml;

        /// <summary>
        /// Temporary setting which change at every global rendering operation
        /// </summary>
        public static Hashtable SessionSettings;
        #endregion

        #region Enum properties
        /// <summary>
        /// Valid values for Renderer.xlsOutputMethod
        /// </summary>
        public enum OutputMethods { 
            /// <summary>
            /// Outputs content as a parsable XML document
            /// </summary>
            Xml,
            /// <summary>
            /// Outputs content as a parsable HTML document
            /// </summary>
            Html,
            /// <summary>
            /// Outputs content as a parsable XHTML document
            /// </summary>
            XHtml 
        }
        #endregion

        #region Internal properties

        /// <summary>
        /// The namespace used to create ASP.Net Web Controls
        /// </summary>
        internal static string aspDotNetNS = "http://schemas.microsoft.com/AspNet/WebControls";
        /// <summary>
        /// The namespace used to create xml elements
        /// </summary>
        internal static string xmlNS = "http://www.w3.org/2000/xmlns/";
        /// <summary>
        /// The namespace used to create Landscape elements
        /// </summary>
        internal static string landscapeNS = "http://land.scape";
        /// <summary>
        /// The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result
        /// </summary>
        internal static System.Web.UI.WebControls.PlaceHolder placeholder;
        internal static Dictionary<string, Elements.Control> PageControls = new Dictionary<string, Elements.Control>();
        private static XsltExtension _Extension;
        internal static XsltExtension Extension
        {
            get
            {
                if (_Extension == null)
                {
                    // initialize custom xslt extension
                    _Extension = new XsltExtension(SessionSettings);
                }

                return _Extension;
            }
        }

        private static XmlUrlResolver _UrlResolver;
        internal static XmlUrlResolver UrlResolver
        {
            get
            {
                if (_UrlResolver == null)
                {
                    _UrlResolver = new TemplateXmlUrlResolver(includeDirs);
                }

                return _UrlResolver;
            }
        }

        private static List<KeyValuePair<int, string>> sourcePaths = Globals.Config.SourceDirs;
        private static List<KeyValuePair<int, string>> includeDirs = Globals.Config.IncludeDirs;
        private static List<KeyValuePair<int, string>> widgetDirs = Globals.Config.WidgetDirs;
        #endregion

        private Renderer()
        {
        }

        #region Configuration Methods
        /// <summary>
        /// Add a source directory for templates during runtime. This directory is used only for this rendering session.
        /// </summary>
        /// <param name="priority">The priority to give to the directory</param>
        /// <param name="path">The path of the source directory</param>
        public static void AddSourceDir(int priority, string path)
        {
            if (sourcePaths.IndexOf(new KeyValuePair<int, string>(priority, path)) < 0)
            {
                sourcePaths.Add(new KeyValuePair<int, string>(priority, path));
            }
        }

        /// <summary>
        /// Add an include directory for xsl:include and xsl:import during runtime. This directory is used only for this rendering session.
        /// </summary>
        /// <param name="priority">The priority to give to the directory</param>
        /// <param name="path">The path of the source directory</param>
        public static void AddIncludeDir(int priority, string path)
        {
            if (includeDirs.IndexOf(new KeyValuePair<int, string>(priority, path)) < 0)
            {
                includeDirs.Add(new KeyValuePair<int, string>(priority, path));
            }
        }
        #endregion

        #region Public render methods
        /// <summary>
        /// Render the template identified by templateName using the specific System.Data.DataSet instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="data">A valid System.Data.DataSet instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>
        /// <returns>The rendered content</returns>
        public static void Render(string templateName, DataSet data, System.Web.UI.WebControls.PlaceHolder contentPlaceholder)
        {
            placeholder = contentPlaceholder;

            // load dataset xml as an XPath Document
            MemoryStream dataStream = new MemoryStream();
            data.WriteXml(dataStream);
            dataStream.Position = 0;
            XPathDocument xmldom = new XPathDocument(dataStream);

            FillContentPlaceholder(Render(templateName, xmldom, null, null));
        }

        /// <summary>
        /// Render the template identified by templateName using the specific System.Xml.XmlDocument instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="xmldoc">A valid System.Xml.XmlDocument instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>/// 
        /// <returns>The rendered content</returns>
        public static void Render(string templateName, XmlDocument xmldoc, System.Web.UI.WebControls.PlaceHolder contentPlaceholder)
        {
            placeholder = contentPlaceholder;

            MemoryStream dataStream = new MemoryStream();
            StreamWriter dataStreamWriter = new StreamWriter(dataStream);
            dataStreamWriter.Write(xmldoc.OuterXml);
            dataStreamWriter.Flush();
            dataStream.Position = 0;
            XPathDocument xmldom = new XPathDocument(dataStream);

            FillContentPlaceholder(Render(templateName, xmldom, null, null));
        }

        /// <summary>
        /// Render the template identified by templateName using the specific System.Xml.XPath.XPathDocument instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="xmldom">A valid System.Xml.XPath.XPathDocument instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>
        /// <returns>The rendered content</returns>
        public static void Render(string templateName, XPathDocument xmldom, System.Web.UI.WebControls.PlaceHolder contentPlaceholder)
        {
            placeholder = contentPlaceholder;

            if (sourcePaths.Count == 0)
            {
                throw new Exception("No source path defined");
            }

            // sort template dirs, include dirs and widget ones by priority descending
            // the first processed item is the one with the highest priority
            sourcePaths.Sort(delegate(KeyValuePair<int, string> cur, KeyValuePair<int, string> next)
            {
                return cur.Key.CompareTo(next.Key);
            });
            includeDirs.Sort(delegate(KeyValuePair<int, string> cur, KeyValuePair<int, string> next)
            {
                return next.Key.CompareTo(cur.Key);
            });
            widgetDirs.Sort(delegate(KeyValuePair<int, string> cur, KeyValuePair<int, string> next)
            {
                return next.Key.CompareTo(cur.Key);
            });

            FillContentPlaceholder(Render(templateName, xmldom, null, null));
        }
        #endregion

        #region Actual render method

        private static string Render(string templateName, XPathDocument xmldom, string placeholderContent, List<KeyValuePair<string, string>> templateParams)
        {
            string templateFile = templateName + "." + xslExtension;
            string templatePath = Globals.LookupFile(templateFile, sourcePaths);

            if (string.IsNullOrEmpty(templatePath))
            {
                throw new Exception("Template '" + templateFile + "' not found after lookup in " + sourcePaths.Count + " directories.");
            }

            try
            {
                DocumentTemplate template = DocumentTemplate.GetTemplate(templatePath);
                OutputMethods currentXslOutputMethod = xslOutputMethod;

                // performs current template transformation
                string buffer = XslTransform(xmldom, template.TemplateCompiledTransform, templateParams);

                // performs master transformation and return result
                if (string.IsNullOrEmpty(template.LayoutName))
                {

                    // FIXME: move this when it does not get repeated N times
                    // FIXME or wait for Xhtml output method implementation (SAXON 2, libxslt :()
                    // fix shortly-closed tags
                    if (template.OutputMethod == OutputMethods.XHtml)
                    {
                        buffer = Regex.Replace(buffer, "<(?<tag>div|span|p|script|select|option|a|ul|li)[:b]*(?<attrs>[^>]*)?/>", "<${tag}${attrs}></${tag}>");
                    }
                }
                else
                {
                    List<KeyValuePair<string, string>> layoutParameters = ProcessLayoutParametes(ref buffer);

                    buffer = Render(template.LayoutName, xmldom, buffer, layoutParameters);
                }

                // ancestor templates processing
                if (placeholderContent != null)
                {
                    XmlDocument layoutDom = new XmlDocument();
                    // disable resolving references to external resources such as DTDs, entities, and schemas. 
                    layoutDom.XmlResolver = null;
                    layoutDom.LoadXml(buffer);

                    ReplacePlaceholder(ref layoutDom, placeholderContent);

                    // overwrites buffer with processed value
                    buffer = layoutDom.OuterXml;
                }

                // writes processed buffer into a MemoryStream for further processing
                MemoryStream streamResult = Globals.GetStreamFromString(buffer);

                // load buffer into XmlDocument
                XmlDocument resultDom = new XmlDocument();
                resultDom.Load(HtmlXmlReader.Create(streamResult, ConformanceLevel.Document));

                // process widgets and user controls
                ReplaceWidgets(ref resultDom, xmldom);
                ParseControls(ref resultDom, xmldom);

                return resultDom.OuterXml;
            }
            catch (Exception ex)
            {
                throw new Exception("Cannot load template '" + templatePath + "'", ex);
            }
        }
        #endregion

        private static string XslTransform(XPathDocument xmldoc, XslCompiledTransform compiledTransform, List<KeyValuePair<string, string>> xslParams)
        {
            XsltArgumentList xsltArgs = new XsltArgumentList();
            xsltArgs.AddExtensionObject("urn:landscape", Extension);

            Dictionary<string, object> xsltExtensions = new Dictionary<string, object>(GetXsltExtensions());

            if (xsltExtensions.Count > 0) {
                foreach (KeyValuePair<string, object> ext in xsltExtensions) {
                    xsltArgs.AddExtensionObject(ext.Key, ext.Value);
                }
            }

            // add specified params
            if(xslParams != null && xslParams.Count > 0){
                foreach (KeyValuePair<string, string> xslParam in xslParams) {
                    xsltArgs.AddParam(xslParam.Key, "", xslParam.Value);
                }
            }

            MemoryStream xsltResult = new MemoryStream();
            XmlWriter xmlWriter = XmlWriter.Create(xsltResult, compiledTransform.OutputSettings);

            compiledTransform.Transform(xmldoc, xsltArgs, xmlWriter);
            xmlWriter.Close();
            xsltResult.Position = 0;
            // read transformation result into a string
            StreamReader resultReader = new StreamReader(xsltResult);

            return resultReader.ReadToEnd();
        }

        private static Dictionary<string, object> GetXsltExtensions() {
            Dictionary<string, object> extensions = new Dictionary<string, object>();

            XsltExtensionCollection configuredExtensions = Globals.Config.XsltExtensions;

            if (configuredExtensions != null && configuredExtensions.Count > 0) {
                foreach (XsltExtensionElement extension in configuredExtensions) {
                    Members.ExtensionObject extensionObject = Members.ExtensionObject.GetExtensionObject(extension);

                    extensions.Add(extensionObject.Alias, extensionObject.Extension);
                }
            }

            return extensions;
        }

        private static XmlAttribute SetAttribute(XmlDocument doc, XmlElement element, string name, string value)
        {
            XmlAttribute attr = doc.CreateAttribute(name);
            attr.Value = value;
            element.Attributes.Append(attr);

            return attr;
        }

        private static XmlNode CreateDocumentFragment(XmlDocument doc, string content)
        {
            XmlDocumentFragment fragment = doc.CreateDocumentFragment();
            fragment.InnerXml = content;
            return fragment;
        }

        internal static void CreateXslOutput(XmlDocument doc, OutputMethods method, string docTypePublic, string docTypeSystem, string encoding, bool omitXmlDeclaration, bool standalone, bool indent, bool force)
        {
            XmlElement outputTag = doc.CreateElement("xsl", "output", xslNS);
            SetAttribute(doc, outputTag, "version", xslVersion);
            SetAttribute(doc, outputTag, "method", (method == OutputMethods.Xml || method == OutputMethods.XHtml? "xml" : "html"));
            //SetAttribute(doc, outputTag, "encoding", encoding);
            SetAttribute(doc, outputTag, "omit-xml-declaration", (omitXmlDeclaration == true ? "yes" : "no"));
            SetAttribute(doc, outputTag, "standalone", (standalone == true ? "yes" : "no"));
            SetAttribute(doc, outputTag, "indent", (indent == true ? "yes" : "no"));
            if (!string.IsNullOrEmpty(docTypePublic)) {
                SetAttribute(doc, outputTag, "doctype-public", docTypePublic);
            }

            if (!string.IsNullOrEmpty(docTypeSystem)){
                SetAttribute(doc, outputTag, "doctype-system", docTypeSystem);
            }

            XmlNodeList xslOutputTags = doc.GetElementsByTagName("output", xslNS);
            if (xslOutputTags.Count > 0)
            {
                XmlNode oldOutputTag = xslOutputTags.Item(0);

                if (force == true)
                {
                    doc.DocumentElement.ReplaceChild(outputTag, oldOutputTag);
                }
            }else {
                XmlNode xslOutputLastPrecedingSibling = doc.DocumentElement.FirstChild;

                // insert just after the last import node, if any
                XmlNodeList xslImports = doc.GetElementsByTagName("import", xslNS);
                if (xslImports.Count > 0)
                {
                    xslOutputLastPrecedingSibling = xslImports.Item(xslImports.Count - 1);
                    doc.DocumentElement.InsertAfter(outputTag, xslOutputLastPrecedingSibling);
                }
                else {
                    doc.DocumentElement.InsertBefore(outputTag, xslOutputLastPrecedingSibling);
                }
                
            }
        }

        private static void ReplacePlaceholder(ref XmlDocument xmlDocument, string xmlContent) {
            XmlNodeList pholders = xmlDocument.GetElementsByTagName("pholder", landscapeNS);

            // tries to substitute placeholder with content
            // if placeholder id doesn't match, just removes it
            if (pholders.Count > 0)
            {
                XmlNode p = pholders.Item(0);

                if (p != null)
                {
                    p.ParentNode.ReplaceChild(CreateDocumentFragment(xmlDocument, xmlContent), p);
                }
                else
                {
                    p.ParentNode.RemoveChild(p);
                }
            }     
        }

        private static bool ReplaceWidgets(ref XmlDocument resultDocument, XPathDocument xmldom) {
            XmlNodeList nodes = resultDocument.GetElementsByTagName("widget", landscapeNS);
            if (nodes.Count > 0)
            {

                // copy nodes to a list in order to replace them seamlessy
                List<XmlNode> widgets = new List<XmlNode>();
                foreach (XmlNode node in nodes)
                {
                    widgets.Add(node);
                }

                foreach (XmlNode node in widgets)
                {
                    Elements.Widget w = new Elements.Widget(node);

                    string widgetFile = w.Name + "." + xslExtension;
                    string widgetPath = Globals.LookupFile(widgetFile, widgetDirs);

                    if (string.IsNullOrEmpty(widgetPath))
                    {
                        throw new Exception("Widget not found after lookup in " + widgetDirs.Count + " dirs: " + widgetFile);
                    }

                    WidgetTemplate template = WidgetTemplate.GetTemplate(widgetPath);

                    string replacement = XslTransform(xmldom, template.TemplateCompiledTransform, w.Params);

                    node.ParentNode.ReplaceChild(CreateDocumentFragment(resultDocument, replacement), node);
                }

                // recursively replace nested widgets
                return ReplaceWidgets(ref resultDocument, xmldom);
            } else {
                return false;
            }
        }

        private static void ParseControls(ref XmlDocument resultDocument, XPathDocument xmldom)
        {
            string userControlsVDir = Globals.Config.Setting("landscapeUserControlsVDir");

            // don't parse controls since there is no valid source directory configured
            if (string.IsNullOrEmpty(userControlsVDir)) return;

            XmlNodeList nodes = resultDocument.GetElementsByTagName("control", landscapeNS);
            if (nodes.Count > 0)
            {

                // copy nodes to a list in order to replace them seamlessy
                List<XmlNode> controls = new List<XmlNode>();
                foreach (XmlNode node in nodes)
                {
                    controls.Add(node);
                }

                foreach (XmlNode node in controls)
                {
                    //TODO: control caching
                    Elements.Control control = new Elements.Control(node);

                    if (!PageControls.ContainsKey(control.ControlId)) {
                        PageControls.Add(control.ControlId, control);
                    }
                }
            }
        }

        private static List<KeyValuePair<string, string>> ProcessLayoutParametes(ref string buffer) {
            List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();

            MemoryStream bufferStream = Globals.GetStreamFromString(buffer);
            XmlDocument bufferDom = new XmlDocument();
            bufferDom.Load(HtmlXmlReader.Create(bufferStream, ConformanceLevel.Fragment));

            XmlNodeList nodes = bufferDom.GetElementsByTagName("param", landscapeNS);

            if (nodes.Count > 0) {
                // copy nodes to a list in order to replace them seamlessy
                List<XmlNode> paramNodes = new List<XmlNode>();
                foreach (XmlNode node in nodes)
                {
                    paramNodes.Add(node);
                }

                foreach(XmlNode paramNode in paramNodes){
                    Elements.Parameter parameter = new Elements.Parameter(paramNode);
                    result.Add(new KeyValuePair<string,string>(parameter.Name, parameter.Value));

                    paramNode.ParentNode.RemoveChild(paramNode);
                }

                buffer = bufferDom.OuterXml;
            }

            return result;

        }

        private static void FillContentPlaceholder(string result) {
            result = result.Replace("xmlns:asp=\"remove\"", "");

            // populates placeholder with rendered content
            placeholder.Controls.Add(placeholder.Page.ParseControl(result));

            // gets a reference to the parsed control
            System.Web.UI.Control parsedControl = placeholder.Controls[0];

            // add user controls to placeholder
            if (PageControls.Count > 0)
            {
                foreach (Elements.Control control in PageControls.Values)
                {
                    System.Web.UI.WebControls.PlaceHolder ucPlaceholder = (System.Web.UI.WebControls.PlaceHolder)Globals.FindControlRecursive(parsedControl, control.PlaceHolderId);

                    if (placeholder.FindControl(control.PlaceHolderId) != null) {
                        System.Web.UI.Control uc = Renderer.placeholder.Page.TemplateControl.LoadControl(control.VirtualPath);
                        uc.ID = control.ControlId;

                        if (control.Properties.Count > 0)
                        {
                            foreach (KeyValuePair<string, string> param in control.Params) {
                                // parameter has a corresponding set property
                                if (control.Properties[param.Key] != null && param.Value.Length > 0)
                                {
                                    string propertyType = control.Properties[param.Key];
                                    object propertyValue = null;

                                    switch (propertyType) { 
                                        case "System.Int32":
                                            propertyValue = Convert.ToInt32(param.Value);
                                            break;
                                        case "System.String":
                                            propertyValue = param.Value;
                                            break;
                                        case "System.Boolean":
                                            int propertyTempValue = Convert.ToInt32(param.Value);
                                            propertyValue = propertyTempValue == 1 ? true : false;
                                            break;
                                    }

                                    // dynamically set object property
                                    if (propertyValue != null) {
                                        uc.GetType().GetProperty(param.Key).SetValue(uc, propertyValue, null);                                    
                                    }
                                }
                            }
                        }

                        ucPlaceholder.Controls.Add(uc);
                    }
                }
            }            
        }
    }
}